package s3_base

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/pkg/errors"
)

const (
	DefaultPartitionThreshold = 8 * 1024 * 1024
)

type PermanentError string

var PermanentErrorMap = map[string]struct{}{
	"InvalidBucketName":                  {},
	"InvalidAccessKeyId":                 {},
	"NoSuchBucket":                       {},
	"IllegalLocationConstraintException": {},
	"SignatureDoesNotMatch":              {},
}

type Client struct {
	s3Client   *s3.S3
	uploader   *s3manager.Uploader
	downloader *s3manager.Downloader
}

type Option func(c *aws.Config)

func WithAKSK(ak, sk string) Option {
	return func(c *aws.Config) {
		c.Credentials = credentials.NewStaticCredentials(ak, sk, "")
	}
}

func WithProvider(pro credentials.Provider) Option {
	return func(c *aws.Config) {
		c.Credentials = credentials.NewCredentials(pro)
	}
}

func WithDisableSSL(dis bool) Option {
	return func(c *aws.Config) {
		c.DisableSSL = aws.Bool(dis)
	}
}

func New(ctx context.Context, region, endPoint string, opts ...Option) (*Client, error) {
	config := &aws.Config{
		Region:   aws.String(region),
		Endpoint: aws.String(endPoint),
	}

	for _, opt := range opts {
		opt(config)
	}

	sess, err := session.NewSession(config)
	if err != nil {
		return nil, errors.Wrap(err, "create s3 client failed")
	}
	return &Client{
		s3Client: s3.New(sess),
		uploader: s3manager.NewUploader(sess, func(u *s3manager.Uploader) {
			u.PartSize = DefaultPartitionThreshold
		}),
		downloader: s3manager.NewDownloader(sess, func(d *s3manager.Downloader) {
			d.PartSize = DefaultPartitionThreshold
		}),
	}, nil
}

func (c *Client) Upload(ctx context.Context, bucket, key string, r io.Reader) error {
	_, err := c.uploader.Upload(&s3manager.UploadInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
		Body:   r,
	})
	return errors.Wrap(parseError(err), "Upload failed")
}

func (c *Client) Download(ctx context.Context, bucket, key string, dst io.WriterAt) error {
	_, err := c.downloader.Download(dst, &s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	return errors.Wrap(parseError(err), "Download failed")
}

func (c *Client) ListPrefix(ctx context.Context, bucket, prefix string) ([]string, error) {
	resp, err := c.s3Client.ListObjectsV2(&s3.ListObjectsV2Input{
		Bucket: aws.String(bucket),
		Prefix: aws.String(prefix),
	})
	if err != nil {
		return nil, errors.Wrap(parseError(err), "ListObjectsV2 failed")
	}
	var keys []string
	for _, v := range resp.Contents {
		keys = append(keys, *v.Key)
	}
	return keys, nil
}

func (c *Client) URL(ctx context.Context, bucket, key string) string {
	req, _ := c.s3Client.GetObjectRequest(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	req.Sign()
	return req.HTTPRequest.URL.String()
}

func (c *Client) PresignURL(ctx context.Context, bucket, key string, duration time.Duration) (string, error) {
	req, _ := c.s3Client.GetObjectRequest(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	uurl, err := req.Presign(duration)
	return url.QueryEscape(uurl), err
}

func (c *Client) ObjectExists(ctx context.Context, bucket, key string) (bool, error) {
	_, err := c.s3Client.HeadObject(&s3.HeadObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey {
			return false, nil
		}
		return false, errors.Wrap(parseError(err), "HeadObject failed")
	}
	return true, nil
}

func parseError(err error) error {
	if aErr, ok := err.(awserr.Error); ok {
		if rf, ok := err.(s3.RequestFailure); ok {
			err = fmt.Errorf("requestID: %s, hostID: %s, StatusCode: %d, errorCode: %s, errorMessage: %s",
				rf.RequestID(), rf.HostID(), rf.StatusCode(), rf.Code(), rf.Message())
		}
		if _, ok2 := PermanentErrorMap[aErr.Code()]; ok2 {
			return errors.WithStack(err)
		}
	}
	return err
}
